|
1
|
|
|
/*! |
|
2
|
|
|
* @package ElkArte Forum |
|
3
|
|
|
* @copyright ElkArte Forum contributors |
|
4
|
|
|
* @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
|
5
|
|
|
* |
|
6
|
|
|
* @version 1.1.9 |
|
7
|
|
|
* |
|
8
|
|
|
* Original code from Aziz, redone and refactored for ElkArte |
|
9
|
|
|
*/ |
|
10
|
|
|
|
|
11
|
|
|
/** global: elk_session_id, elk_session_var, elk_scripturl */ |
|
12
|
|
|
|
|
13
|
|
|
/** |
|
14
|
|
|
* This javascript searches the message for video links and replaces them |
|
15
|
|
|
* with a clickable preview thumbnail of the video. Once the image is clicked |
|
16
|
|
|
* the video is embedded in to the page to play. |
|
17
|
|
|
* |
|
18
|
|
|
* Currently, works with YouTube, Vimeo, TikTok and DailyMotion |
|
19
|
|
|
* |
|
20
|
|
|
*/ |
|
21
|
|
|
(function ($) |
|
22
|
|
|
{ |
|
23
|
|
|
'use strict'; |
|
24
|
|
|
|
|
25
|
|
|
/** |
|
26
|
|
|
* @param {object} oInstanceSettings holds the text strings to use in the html created |
|
27
|
|
|
* @param {int} msgid optional to only search for links in a specific id |
|
28
|
|
|
*/ |
|
29
|
|
|
$.fn.linkifyvideo = function (oInstanceSettings, msgid) |
|
30
|
|
|
{ |
|
31
|
|
|
let oDefaultsSettings = { |
|
32
|
|
|
embed_limit: 25, |
|
33
|
|
|
preview_image: '', |
|
34
|
|
|
ctp_video: '', |
|
35
|
|
|
hide_video: '', |
|
36
|
|
|
youtube: '', |
|
37
|
|
|
vimeo: '', |
|
38
|
|
|
dailymotion: '', |
|
39
|
|
|
tiktok: '' |
|
40
|
|
|
}; |
|
41
|
|
|
|
|
42
|
|
|
// Account for user options |
|
43
|
|
|
let oSettings = $.extend({}, oDefaultsSettings, oInstanceSettings || {}); |
|
44
|
|
|
|
|
45
|
|
|
/** |
|
46
|
|
|
* Replaces the image with the created embed code to show the video |
|
47
|
|
|
* Called from click event attached to the image |
|
48
|
|
|
* |
|
49
|
|
|
* @param {string} tag anchor tag we are replacing with the embed tag |
|
50
|
|
|
* @param {string} eURL the load or place source link |
|
51
|
|
|
*/ |
|
52
|
|
|
function showEmbed(tag, eURL) |
|
53
|
|
|
{ |
|
54
|
|
|
$(tag).html(embed_html.replace('{src}', eURL)); |
|
|
|
|
|
|
55
|
|
|
} |
|
56
|
|
|
|
|
57
|
|
|
/** |
|
58
|
|
|
* Shows the video image and sets up the link |
|
59
|
|
|
* Sets click event to load video sites embed code |
|
60
|
|
|
* |
|
61
|
|
|
* @param {object} a videoID link |
|
62
|
|
|
* @param {string} src source of image |
|
63
|
|
|
* @param {string} eURLa play link |
|
64
|
|
|
*/ |
|
65
|
|
|
function getIMG(a, src, eURLa) |
|
66
|
|
|
{ |
|
67
|
|
|
return $('' + |
|
68
|
|
|
'<div class="elk_video">' + |
|
69
|
|
|
' <a href="' + a.href + '">' + |
|
70
|
|
|
' <img class="elk_video_preview" alt="' + oSettings.preview_image + '" ' + 'title="' + oSettings.ctp_video + '" src="' + src + '"/>' + |
|
71
|
|
|
' </a>' + |
|
72
|
|
|
'</div>') |
|
73
|
|
|
.on('click', function (e) |
|
74
|
|
|
{ |
|
75
|
|
|
e.preventDefault(); |
|
76
|
|
|
let tag = this; |
|
77
|
|
|
showEmbed(tag, eURLa); |
|
78
|
|
|
} |
|
79
|
|
|
); |
|
80
|
|
|
} |
|
81
|
|
|
|
|
82
|
|
|
/** |
|
83
|
|
|
* Returns a linked preview image. Click on the image to load the player. |
|
84
|
|
|
* |
|
85
|
|
|
* @param {string} a link tag of the video |
|
86
|
|
|
* @param {string} src link of the preview image |
|
87
|
|
|
* @param {string} eURLa single click event play video |
|
88
|
|
|
*/ |
|
89
|
|
|
function embedIMG(a, src, eURLa) |
|
90
|
|
|
{ |
|
91
|
|
|
return getIMG(a, src, eURLa); |
|
92
|
|
|
} |
|
93
|
|
|
|
|
94
|
|
|
/** |
|
95
|
|
|
* Creates and inserts a document fragment. Doing this vs inner/outer HTML ensures that any script |
|
96
|
|
|
* tags in the embed code will execute. |
|
97
|
|
|
* |
|
98
|
|
|
* @param {Element} a the link we are working with |
|
99
|
|
|
* @param {object} data the data from the ajax call |
|
100
|
|
|
*/ |
|
101
|
|
|
function createFragment(a, data) |
|
|
|
|
|
|
102
|
|
|
{ |
|
103
|
|
|
// Since data.html may contain a script tag that needs to run, we have to add it like this |
|
104
|
|
|
let parent = a.parentNode, |
|
105
|
|
|
frag = document.createRange().createContextualFragment('<div class="elk_video">' + data.html + '</div>'); |
|
106
|
|
|
|
|
107
|
|
|
parent.parentNode.appendChild(frag); |
|
108
|
|
|
parent.nextSibling.outerHTML = ''; |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
// The embed code |
|
112
|
|
|
let domain_regex = /^[^:]*:\/\/(?:www\.)?([^\/]+)(\/.*)$/, |
|
113
|
|
|
embedded_count = 0, |
|
114
|
|
|
embed_html = '<iframe width="640" height="360" style="border: none;margin: 0 auto;aspect-ratio 16 / 9;max-width: 100%; max-height: 360px;" src="{src}" frameborder="0" data-autoplay="true" allow="fullscreen" loading="lazy" type="text/html"></iframe>', |
|
|
|
|
|
|
115
|
|
|
handlers = {}, |
|
116
|
|
|
imgHandlers = {}, |
|
117
|
|
|
logos = { |
|
118
|
|
|
tiktok: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23fff' fill-opacity='.01' d='M0 0h48v48H0z'/%3E%3Cpath fill='%232F88FF' stroke='%23000' stroke-linejoin='round' stroke-width='3.833' d='M21.358 19.14c-5.888-.284-9.982 1.815-12.28 6.299-3.446 6.724-.597 17.728 10.901 17.728 11.499 0 11.831-11.111 11.831-12.276V17.876c2.46 1.557 4.533 2.495 6.221 2.813 1.688.317 2.76.458 3.219.422v-6.476c-1.561-.188-2.911-.547-4.05-1.076-1.709-.794-5.096-2.997-5.096-6.226.002.016.002-.817 0-2.499h-7.118c-.021 15.816-.021 24.502 0 26.058.032 2.334-1.779 5.6-5.45 5.6-3.672 0-5.482-3.263-5.482-5.367 0-1.288.442-3.155 2.271-4.538 1.085-.82 2.59-1.147 5.033-1.147V19.14Z'/%3E%3C/svg%3E", |
|
119
|
|
|
vimeo: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 455.731 455.731'%3E%3Cpath fill='%231ab7ea' d='M0 0h455.731v455.731H0z'/%3E%3Cpath fill='%23fff' d='m49.642 157.084 17.626 22.474s22.033-17.186 29.965-17.186c4.927 0 15.423 5.729 22.033 25.558 6.61 19.83 34.441 122.62 36.134 127.351 7.607 21.26 17.626 60.811 48.473 66.54s70.065-25.558 91.657-48.473c21.592-22.914 106.64-120.741 110.165-179.349 3.26-54.191-14.517-66.765-22.474-71.828-14.542-9.254-38.778-12.338-61.692-4.407s-57.726 33.931-66.98 80.2c0 0 31.287-11.457 42.744-.441s8.373 35.253-1.322 53.32-37.015 59.93-47.151 61.252c-10.135 1.322-18.067-18.508-19.389-23.796-1.322-5.288-18.067-77.997-24.236-120.3s-33.049-49.354-45.829-49.354c-12.779.001-34.812 9.696-109.724 78.439z'/%3E%3C/svg%3E", |
|
120
|
|
|
dailymotion: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%230066DC' fill-rule='evenodd' d='M0 512h512V0H0v512Zm441.5-68.635h-76.314v-29.928c-23.443 22.945-47.385 31.424-79.308 31.424-32.421 0-60.354-10.474-83.797-31.424-30.926-27.433-46.887-63.346-46.887-105.245 0-38.407 14.965-72.823 42.896-99.758 24.94-24.44 55.367-36.91 89.284-36.91 32.422 0 57.361 10.973 75.318 33.917V88.724L441.5 72.395v370.97Zm-141.157-202.01c-37.41 0-66.339 30.426-66.339 66.338 0 37.41 28.93 65.841 69.332 65.841 33.918 0 62.349-27.932 62.349-64.843 0-38.406-28.431-67.336-65.342-67.336Z'/%3E%3C/svg%3E", |
|
121
|
|
|
}; |
|
122
|
|
|
|
|
123
|
|
|
// Get a TikTok video thumbnail and embed data |
|
124
|
|
|
imgHandlers.getTikTokEmbed = function(eURL, callback) |
|
125
|
|
|
{ |
|
126
|
|
|
$.getJSON(eURL, {format: 'json'}, |
|
127
|
|
|
function(data) |
|
128
|
|
|
{ |
|
129
|
|
|
if (typeof data.html === 'undefined') |
|
130
|
|
|
{ |
|
131
|
|
|
data.thumbnail_url = logos.tiktok; |
|
132
|
|
|
data.html = ''; |
|
133
|
|
|
} |
|
134
|
|
|
|
|
135
|
|
|
callback(data); |
|
136
|
|
|
}); |
|
137
|
|
|
}; |
|
138
|
|
|
|
|
139
|
|
|
// Get a dailymotion video thumbnail |
|
140
|
|
|
imgHandlers.getDailymotionIMG = function(eURL, callback) |
|
141
|
|
|
{ |
|
142
|
|
|
$.getJSON(eURL, {}, |
|
143
|
|
|
function(data) |
|
144
|
|
|
{ |
|
145
|
|
|
if (typeof data.thumbnail_480_url !== 'undefined') |
|
146
|
|
|
{ |
|
147
|
|
|
callback(data.thumbnail_480_url); |
|
148
|
|
|
} |
|
149
|
|
|
else |
|
150
|
|
|
{ |
|
151
|
|
|
callback(logos.dailymotion); |
|
152
|
|
|
} |
|
153
|
|
|
}); |
|
154
|
|
|
}; |
|
155
|
|
|
|
|
156
|
|
|
// Get a Vimeo video thumbnail |
|
157
|
|
|
imgHandlers.getVimeoIMG = function(videoID, callback) |
|
158
|
|
|
{ |
|
159
|
|
|
$.getJSON(videoID, {format: 'json'}, |
|
160
|
|
|
function(data) |
|
161
|
|
|
{ |
|
162
|
|
|
if (typeof data[0].thumbnail_large !== 'undefined') |
|
163
|
|
|
{ |
|
164
|
|
|
callback(data[0].thumbnail_large); |
|
165
|
|
|
} |
|
166
|
|
|
else |
|
167
|
|
|
{ |
|
168
|
|
|
callback(logos.vimeo); |
|
169
|
|
|
} |
|
170
|
|
|
}); |
|
171
|
|
|
}; |
|
172
|
|
|
|
|
173
|
|
|
// Youtube and variants |
|
174
|
|
|
handlers['youtube.com'] = function (path, a) |
|
175
|
|
|
{ |
|
176
|
|
|
let videoID = path.match(/\bv[=/]([^&#?$]+)/i) || path.match(/#p\/(?:a\/)?[uf]\/\d+\/([^?$]+)/i) || path.match(/(?:\/)([\w-]{11})/i); |
|
177
|
|
|
|
|
178
|
|
|
if (!videoID || !(videoID = videoID[1])) |
|
179
|
|
|
{ |
|
180
|
|
|
return; |
|
|
|
|
|
|
181
|
|
|
} |
|
182
|
|
|
|
|
183
|
|
|
// There are two types of YouTube timestamped links |
|
184
|
|
|
// http://youtu.be/lLOE3fBZcUU?t=1m37s when you click share underneath the video |
|
185
|
|
|
// http://youtu.be/lLOE3fBZcUU?t=97 when you right click on a video and choose "Copy video URL at current time" |
|
186
|
|
|
// For embedding, you need to use "?start=97" instead, so we have to convert t=1m37s to seconds while also supporting t=97 |
|
187
|
|
|
let startAt = path.match(/t=(?:([1-9]{1,2})h)?(?:([1-9]{1,2})m)?(?:([1-9]+)s?)/), |
|
188
|
|
|
startAtPar = ''; |
|
189
|
|
|
|
|
190
|
|
|
if (startAt) |
|
191
|
|
|
{ |
|
192
|
|
|
let startAtSeconds = 0; |
|
193
|
|
|
|
|
194
|
|
|
// Hours |
|
195
|
|
|
if (typeof startAt[1] !== 'undefined') |
|
196
|
|
|
{ |
|
197
|
|
|
startAtSeconds += parseInt(startAt[1]) * 3600; |
|
198
|
|
|
} |
|
199
|
|
|
|
|
200
|
|
|
// Minutes |
|
201
|
|
|
if (typeof startAt[2] !== 'undefined') |
|
202
|
|
|
{ |
|
203
|
|
|
startAtSeconds += parseInt(startAt[2]) * 60; |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
// Seconds |
|
207
|
|
|
if (typeof startAt[3] !== 'undefined') |
|
208
|
|
|
{ |
|
209
|
|
|
startAtSeconds += parseInt(startAt[3]); |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
startAtPar = '&start=' + startAtSeconds.toString(); |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
let embedURL = '//www.youtube-nocookie.com/embed/' + videoID + '?rel=0' + startAtPar, |
|
216
|
|
|
tag = embedIMG(a, '//i.ytimg.com/vi/' + videoID + '/sddefault.jpg', embedURL + '&autoplay=1'); |
|
217
|
|
|
|
|
218
|
|
|
return [oSettings.youtube, tag]; |
|
219
|
|
|
}; |
|
220
|
|
|
handlers['m.youtube.com'] = handlers['youtube.com']; |
|
221
|
|
|
handlers['youtu.be'] = handlers['youtube.com']; |
|
222
|
|
|
|
|
223
|
|
|
// Vimeo |
|
224
|
|
|
handlers['vimeo.com'] = function (path, a) |
|
225
|
|
|
{ |
|
226
|
|
|
let videoID = path.match(/^\/(\d+)/i); |
|
227
|
|
|
|
|
228
|
|
|
if (!videoID || !(videoID = videoID[1])) |
|
229
|
|
|
{ |
|
230
|
|
|
return; |
|
|
|
|
|
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
let embedURL = '//player.vimeo.com/video/' + videoID, |
|
234
|
|
|
imgURL = '//vimeo.com/api/v2/video/' + videoID + '.json', |
|
235
|
|
|
tag; |
|
236
|
|
|
|
|
237
|
|
|
tag = embedIMG(a, logos.vimeo, embedURL + '?autoplay=1'); |
|
238
|
|
|
|
|
239
|
|
|
// Get the preview image / embed tag |
|
240
|
|
|
imgHandlers.getVimeoIMG(imgURL, function (img) |
|
241
|
|
|
{ |
|
242
|
|
|
$(a).parent().next().find("img").attr("src", img); |
|
243
|
|
|
}); |
|
244
|
|
|
|
|
245
|
|
|
return [oSettings.vimeo, tag]; |
|
246
|
|
|
}; |
|
247
|
|
|
|
|
248
|
|
|
// Dailymotion |
|
249
|
|
|
handlers['dailymotion.com'] = function (path, a) |
|
250
|
|
|
{ |
|
251
|
|
|
let videoID = path.match(/^\/video\/([a-z0-9]{1,18})/i); |
|
252
|
|
|
|
|
253
|
|
|
if (!videoID || videoID[1] === '') |
|
254
|
|
|
{ |
|
255
|
|
|
return; |
|
|
|
|
|
|
256
|
|
|
} |
|
257
|
|
|
|
|
258
|
|
|
let embedURL = '//dailymotion.com/embed/video/' + videoID[1], |
|
259
|
|
|
imgURL = '//api.dailymotion.com/video/' + videoID[1] + '?fields=thumbnail_480_url', |
|
260
|
|
|
tag; |
|
261
|
|
|
|
|
262
|
|
|
tag = embedIMG(a, logos.dailymotion, embedURL + '?related=0&autoplay=1'); |
|
263
|
|
|
|
|
264
|
|
|
// Get the preview image or embed tag |
|
265
|
|
|
imgHandlers.getDailymotionIMG(imgURL, function (img) |
|
266
|
|
|
{ |
|
267
|
|
|
$(a).parent().next().find('img').attr('src', img); |
|
268
|
|
|
}); |
|
269
|
|
|
|
|
270
|
|
|
return [oSettings.dailymotion, tag]; |
|
271
|
|
|
}; |
|
272
|
|
|
|
|
273
|
|
|
// TikTok |
|
274
|
|
|
handlers['tiktok.com'] = function (path, a) |
|
275
|
|
|
{ |
|
276
|
|
|
let videoID = path.match(/^\/@([0-9A-Za-z_\-.]*)\/video\/([0-9]*)/i); |
|
277
|
|
|
|
|
278
|
|
|
if (!videoID) |
|
279
|
|
|
{ |
|
280
|
|
|
return; |
|
|
|
|
|
|
281
|
|
|
} |
|
282
|
|
|
|
|
283
|
|
|
let embedURL = '//www.tiktok.com/oembed?url=https://www.tiktok.com/@' + videoID[1] + '/video/' + videoID[2], |
|
284
|
|
|
tag; |
|
285
|
|
|
|
|
286
|
|
|
imgHandlers.getTikTokEmbed(embedURL, function (data) |
|
287
|
|
|
{ |
|
288
|
|
|
$(a).parent().next().find('img').attr('src', data.thumbnail_url); |
|
289
|
|
|
a.embedURL = data.html; |
|
290
|
|
|
}); |
|
291
|
|
|
|
|
292
|
|
|
tag = embedIMG(a, logos.tiktok, embedURL); |
|
293
|
|
|
|
|
294
|
|
|
// Change the default click event to one that replaces the markup, also prepare for a 9/16 video |
|
295
|
|
|
tag.off('click', '**' , false); |
|
296
|
|
|
tag.on('click', function (e) |
|
297
|
|
|
{ |
|
298
|
|
|
e.preventDefault(); |
|
299
|
|
|
let load = $(a).parent(); |
|
300
|
|
|
load.addClass('portrait'); |
|
301
|
|
|
|
|
302
|
|
|
load.next().replaceWith('<div class="elk_video portrait">' + a.embedURL + '</div>'); |
|
303
|
|
|
}); |
|
304
|
|
|
|
|
305
|
|
|
return [oSettings.tiktok, tag]; |
|
306
|
|
|
}; |
|
307
|
|
|
|
|
308
|
|
|
// --------------------------------------------------------------------------- |
|
309
|
|
|
// Get the bbc_link links in the id="msg_1234 divs. |
|
310
|
|
|
let links; |
|
311
|
|
|
|
|
312
|
|
|
if (typeof msgid !== 'undefined') |
|
313
|
|
|
{ |
|
314
|
|
|
links = document.querySelectorAll('#' + msgid + ' a.bbc_link'); |
|
315
|
|
|
} |
|
316
|
|
|
else |
|
317
|
|
|
{ |
|
318
|
|
|
links = document.querySelectorAll('[id^=msg_] a.bbc_link'); |
|
319
|
|
|
} |
|
320
|
|
|
|
|
321
|
|
|
// Create the show/hide button |
|
322
|
|
|
let showhideBtn = $('' + |
|
323
|
|
|
'<a class="floatright" title="' + oSettings.hide_video + '">' + |
|
324
|
|
|
' <i class="icon icon-small i-chevron-up" alt=">"></i>' + |
|
325
|
|
|
'</a>') |
|
326
|
|
|
.on('click', function() |
|
327
|
|
|
{ |
|
328
|
|
|
let $img = $(this).find("i"), // The open / close icon |
|
329
|
|
|
$vid = $(this).parent().next(); // The immediate elk_video div |
|
330
|
|
|
|
|
331
|
|
|
// Toggle slide the video and change the icon |
|
332
|
|
|
$img.attr("class", "icon icon-small " + ($vid.is(":hidden") !== true ? "i-chevron-down" : "i-chevron-up")); |
|
333
|
|
|
$vid.slideToggle(); |
|
334
|
|
|
}); |
|
335
|
|
|
|
|
336
|
|
|
// Loop though each link |
|
337
|
|
|
links.forEach((link) => |
|
338
|
|
|
{ |
|
339
|
|
|
let tag = link, |
|
340
|
|
|
text = tag.innerText || tag.textContent || ''; |
|
341
|
|
|
|
|
342
|
|
|
// Ignore in sentences |
|
343
|
|
|
if (tag.previousSibling && tag.previousSibling.nodeName === '#text' && tag.previousSibling.nodeValue !== ' ') |
|
344
|
|
|
{ |
|
345
|
|
|
return; |
|
346
|
|
|
} |
|
347
|
|
|
|
|
348
|
|
|
// Ignore in quotes and signatures |
|
349
|
|
|
if ("bbc_quote;signature".indexOf(tag.parentNode.className) !== -1) |
|
350
|
|
|
{ |
|
351
|
|
|
return; |
|
352
|
|
|
} |
|
353
|
|
|
|
|
354
|
|
|
// No href or inner text not equal to href attr then we move along |
|
355
|
|
|
if (tag.href === "" || tag.href.indexOf(text) !== 0) |
|
356
|
|
|
{ |
|
357
|
|
|
return; |
|
358
|
|
|
} |
|
359
|
|
|
|
|
360
|
|
|
// Get domain and validate we know how to handle it |
|
361
|
|
|
let m = tag.href.match(domain_regex), |
|
362
|
|
|
handler = null, |
|
|
|
|
|
|
363
|
|
|
args = null; |
|
|
|
|
|
|
364
|
|
|
|
|
365
|
|
|
// One of our video provider domains? |
|
366
|
|
|
if (embedded_count < oSettings.embed_limit && m !== null && typeof handlers[m[1]] !== 'undefined' && handlers[m[1]] !== null) |
|
367
|
|
|
{ |
|
368
|
|
|
// Call the handler and get the tag to insert |
|
369
|
|
|
handler = handlers[m[1]]; |
|
370
|
|
|
|
|
371
|
|
|
args = handler(m[2], tag); |
|
372
|
|
|
if (args) |
|
373
|
|
|
{ |
|
374
|
|
|
embedded_count++; |
|
375
|
|
|
$(tag).wrap('<div class="elk_video_container">'); |
|
376
|
|
|
$(tag).wrap('<div class="elk_videoheader">').text(args[0]).after(showhideBtn.clone(true)); |
|
377
|
|
|
$(tag).parent().after(args[1]); |
|
378
|
|
|
} |
|
379
|
|
|
} |
|
380
|
|
|
}); |
|
381
|
|
|
}; |
|
382
|
|
|
})(jQuery); |
|
383
|
|
|
|